package com.google.code.sms.service;

import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;

import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang.StringUtils;

import com.google.code.sms.Logger;
import com.google.code.sms.dao.AudioMetaDataDao;
import com.google.code.sms.dao.DirectoryMetaDataDao;
import com.google.code.sms.dao.MediaElementDao;
import com.google.code.sms.dao.VideoMetaDataDao;
import com.google.code.sms.domain.MediaElement;
import com.google.code.sms.domain.MediaElement.MediaElementType;
import static com.google.code.sms.domain.MediaElement.MediaElementType.AUDIO;
import static com.google.code.sms.domain.MediaElement.MediaElementType.DIRECTORY;
import static com.google.code.sms.domain.MediaElement.MediaElementType.MEDIA_DIRECTORY;
import static com.google.code.sms.domain.MediaElement.MediaElementType.VIDEO;
import com.google.code.sms.domain.MediaElementComparator;
import com.google.code.sms.service.metadata.AudioParser;
import com.google.code.sms.service.metadata.MetaData.DirectoryMediaType;
import com.google.code.sms.service.metadata.MetaData;
import com.google.code.sms.service.metadata.VideoParser;
import com.google.code.sms.util.FileUtil;

/**
 * Provides services for instantiating and caching media elements and cover art.
 *
 * @author Scott Ware
 */
public class MediaElementService {

    private static final Logger LOG = Logger.getLogger("MediaElementService");
    private SecurityService securityService;
    private SettingsService settingsService;
    private MediaElementDao mediaElementDao;
    private AudioMetaDataDao audioMetaDataDao;
    private VideoMetaDataDao videoMetaDataDao;
    private DirectoryMetaDataDao directoryMetaDataDao;
    private AudioParser audioParser;
    private VideoParser videoParser;

    /**
     * Returns a media element instance for the given file.
     *
     * @param file A file on the local file system.
     * @return A media element instance, or null if not found.
     * @throws SecurityException if access is denied to the given file.
     */
    public MediaElement getMediaElement(File file) {

        // Check file is readable
        if (!securityService.isReadAllowed(file))
        {
            throw new SecurityException("Access denied to file: " + file);
        }

        // Get Media Element from Database
        MediaElement mediaElement = mediaElementDao.getMediaElement(file.getPath());
        
        if(mediaElement == null)
        {
            return null;
        }
        
        // Retrieve Meta-data
        mediaElement.setMetaData(getMetaData(mediaElement));
        
        return mediaElement;
    }

    /**
     * Returns a media element instance for the given path name.
     *
     * @param pathName A path name for a file on the local file system.
     * @return A media element instance.
     * @throws SecurityException if access is denied to the given file.
     */
    public MediaElement getMediaElement(String pathName) {
        return getMediaElement(new File(pathName));
    }

    /**
     * Returns a media element instance for the given ID.
     *
     * @param id An ID which relates to an existing media element.
     * @return A media element instance.
     * @throws SecurityException if access is denied to the given file.
     */
    public MediaElement getMediaElement(int id) {
        
        MediaElement mediaElement = mediaElementDao.getMediaElement(id);
        
        if (mediaElement == null)
        {
            return null;
        }

        if (!securityService.isReadAllowed(mediaElement.getFile()))
            throw new SecurityException("Access denied to file " + mediaElement);
        
        // Retrieve Meta-data
        mediaElement.setMetaData(getMetaData(mediaElement));

        return mediaElement;
    }
     
    /**
     * Returns the Media Element which relates to the parent directory of a given Media Element.
     * 
     * @param mediaElement The Media Element to be processed.
     * @return The Media Element which relates to the parent directory of the given Media Element.
     */
    public MediaElement getParentOf(MediaElement mediaElement) 
    {
        if (mediaElement.getParentPath().isEmpty())
        {
            return null;
        }
        
        return getMediaElement(mediaElement.getParentPath());
    }

    /**
     * Returns all media elements that are children of a given parent path.
     *
     * @param includeAudio Whether files should be included in the result.
     * @param includeVideo Whether files should be included in the result.
     * @param includeDirectories Whether directories should be included in the
     * result.
     * @param sort Whether to sort elements in the same directory.
     * @return All children media files.
     */
    public List<MediaElement> getChildrenOf(String parentPath, boolean includeAudio, boolean includeVideo, boolean includeDirectories, boolean sort) 
    {
        List<MediaElement> result = new ArrayList<>();
        
        for (MediaElement child : mediaElementDao.getChildrenOf(parentPath)) 
        {
            // Retrieve Meta-data
            child.setMetaData(getMetaData(child));
            
            // Determine if media element should be added to the result
            if (child.isDirectory() && includeDirectories) 
            {
                result.add(child);
            }
            
            if (child.isAudio() && includeAudio) {
                result.add(child);
            }
            
            if (child.isVideo() && includeVideo) {
                result.add(child);
            }
        }

        if (sort)
        {
            Comparator<MediaElement> comparator = new MediaElementComparator();
            Set<MediaElement> set = new TreeSet<>(comparator);
            set.addAll(result);
            result = new ArrayList<>(set);
        }

        return result;
    }

    /**
     * Returns the most frequently played.
     *
     * @param offset Number of elements to skip.
     * @param count Maximum number of elements to return.
     * @return The most frequently played elements.
     */
    public List<MediaElement> getMostFrequentlyPlayed(int offset, int count)
    {
        List<MediaElement> result = new ArrayList<>();
        
        // Retrieve Meta-data for each media element
        for (MediaElement child : mediaElementDao.getFrequentlyPlayedDirectoryElements(offset, count)) 
        {
            child.setMetaData(getMetaData(child));
            result.add(child);
        }
        
        return result;
    }

    /**
     * Returns the most recently played.
     *
     * @param offset Number of elements to skip.
     * @param count Maximum number of elements to return.
     * @return The most recently played elements.
     */
    public List<MediaElement> getMostRecentlyPlayed(int offset, int count)
    {   
        List<MediaElement> result = new ArrayList<>();
        
        // Retrieve Meta-data for each media element
        for (MediaElement child : mediaElementDao.getRecentlyPlayedDirectoryElements(offset, count)) 
        {
            child.setMetaData(getMetaData(child));
            result.add(child);
        }
        
        return result;
    }

    /**
     * Returns the most recently added.
     *
     * @param offset Number of elements to skip.
     * @param count Maximum number of elements to return.
     * @return The most recently added elements.
     */
    public List<MediaElement> getMostRecentlyAdded(int offset, int count)
    {
        List<MediaElement> result = new ArrayList<>();
        
        for (MediaElement child : mediaElementDao.getRecentlyAddedDirectoryElements(offset, count)) 
        {
            // Retrieve Meta-data for each media element
            child.setMetaData(getMetaData(child));
            result.add(child);
        }
        
        return result;
    }

    /**
     * Returns all directory elements in alphabetical order.
     *
     *
     * @param offset Number of directory elements to skip.
     * @param count Maximum number of directory elements to return.
     * @return Directory elements in alphabetical order.
     */
    public List<MediaElement> getAlphabeticalDirectoryElements(int offset, int count)
    {
        List<MediaElement> result = new ArrayList<>();
        
        // Retrieve Meta-data for each media element
        for (MediaElement child : mediaElementDao.getAlphabeticalDirectoryElements(offset, count)) 
        {
            child.setMetaData(getMetaData(child));
            result.add(child);
        }
        
        return result;
    }

    // Create a media element for a given file/folder
    public boolean createOrUpdateMediaElement(File file, Date lastScanned) 
    {
        // Check that the file type is supported
        MediaElementType elementType = getMediaElementType(file);
        
        if(elementType == null)
            return false;
        
        // Load existing media element for the given file/folder if it exists
        MediaElement existingElement = mediaElementDao.getMediaElement(file.getPath());
        
        // Check if the media element requires updating
        Date lastModified = new Date(file.lastModified());
        
        // If the file already has an associated media element, simply update the last scanned field
        if(existingElement != null && existingElement.getChanged().getTime() >= lastModified.getTime())
        {
            mediaElementDao.markPresent(existingElement.getPath(), lastScanned);
            return false;
        }
        
        // Create a new media element if the media element does not exist or needs updating
        MediaElement mediaElement = new MediaElement();
        
        // Set MediaElement parameters
        mediaElement.setMediaElementType(elementType);
        mediaElement.setName(file.getName());
        mediaElement.setPath(file.getPath());
        mediaElement.setFolder(securityService.getRootFolderForFile(file));
        mediaElement.setParentPath(file.getParent());
        mediaElement.setParentName(getHighLevelDirectoryName(file));
        mediaElement.setPlayCount(existingElement == null ? 0 : existingElement.getPlayCount());
        mediaElement.setLastPlayed(existingElement == null ? null : existingElement.getLastPlayed());
        mediaElement.setCreated(lastModified);
        mediaElement.setChanged(lastModified);
        mediaElement.setLastScanned(lastScanned);
        mediaElement.setChildrenLastUpdated(new Date(0));
        mediaElement.setPresent(true);

        if (file.isFile()) 
        {
            // Set file specific parameters
            mediaElement.setFormat(StringUtils.trimToNull(StringUtils.lowerCase(FilenameUtils.getExtension(file.getPath()))));
            mediaElement.setFileSize(file.length());
        } 

        // Apply changes to database
        mediaElementDao.createOrUpdate(mediaElement);
        
        // Get media element from database with ID
        mediaElement = mediaElementDao.getMediaElement(file.getPath());
        
        if(mediaElement == null)
        {
            LOG.error("Failed to load media element from database for file: " + file);
            return false;
        }
        
        // Update media element meta-data
        switch(mediaElement.getMediaElementType())
        {
            case AUDIO:
                createOrUpdateAudioMetaData(mediaElement);
                break;
            case VIDEO:
                createOrUpdateVideoMetaData(mediaElement);
                break;
            case DIRECTORY: case MEDIA_DIRECTORY:
                createOrUpdateDirectoryMetaData(mediaElement);
                break;   
        }

        return true;
    }
    
    private boolean createOrUpdateAudioMetaData(MediaElement element) 
    {
        // Make sure that the media element exists.
        if(element == null)
        {
            return false;
        }
        
        // Create new meta-data
        MetaData metadata;
        
        // Parse file
        metadata = audioParser.parseMetaData(element.getFile());
        
        // Add extra meta-data for file
        metadata.setMediaElementID(element.getId());
        metadata.setCoverArtPath(findCoverArt(element.getParentPath()));

        // Update database
        audioMetaDataDao.createOrUpdate(metadata);
        
        return true;
    }
    
    private boolean createOrUpdateVideoMetaData(MediaElement element) {
        
        // Make sure that the media element exists.
        if(element == null)
            return false;
        
        // Create new meta-data and parser
        MetaData metadata;
        
        // Parse file
        metadata = videoParser.parseMetaData(element.getFile());
        
        // Add extra meta-data for file
        metadata.setMediaElementID(element.getId());
        metadata.setCoverArtPath(findCoverArt(element.getParentPath()));
        metadata.setPosterPath(findPoster(element.getParentPath()));
        metadata.setSubtitlesPath(findSubtitles(element.getParentPath()));

        // Update database
        videoMetaDataDao.createOrUpdate(metadata);
        
        return true;
    }
    
    private boolean createOrUpdateDirectoryMetaData(MediaElement element) 
    { 
        // Make sure that the media element exists.
        if(element == null)
            return false;
        
        MetaData metadata = new MetaData();
        
        // Obtain meta-data for directory
        metadata.setMediaElementID(element.getId());
        metadata.setCoverArtPath(findCoverArt(element.getPath()));
        metadata.setDirectoryMediaType(getDirectoryMediaType(element.getFile()));
        
        // Update database
        directoryMetaDataDao.createOrUpdate(metadata);
        
        return true;
    }
    
    /**
     * Determines what type of media is present in the given directory.
     * 
     * @param directory The directory to scan for media.
     * @return The type of media files present in the directory.
     */
    private DirectoryMediaType getDirectoryMediaType(File directory)
    {
        int audio = 0;
        int video = 0;
        
        for (File file : directory.listFiles()) 
        {
            if (file.isFile() && !FileUtil.isHidden(file)) 
            {
                if(isAudioFile(file.getPath()))
                    audio++;
                else if(isVideoFile(file.getPath()))
                    video++;
            }
        }
        
        if(audio > 0 && video > 0)
            return DirectoryMediaType.MIXED;
        
        if(audio > 0)
            return DirectoryMediaType.AUDIO;
        
        if(video > 0)
            return DirectoryMediaType.VIDEO;
        
        
        return DirectoryMediaType.NONE;
    }
    
    /**
     * Detects a common year for media elements inside a given media directory.
     * 
     */
    private Integer getDirectoryYear(MediaElement directory)
    {
        // First check this is a media directory
        if(!directory.isMediaDirectory())
        {
            return null;
        }
        
        // Variables for storing an initial and current year and determining if a common year is found.
        Integer year = 0, currentYear;
        Boolean commonYear = true;
        
        // Cycle through child media elements (audio, video) in the given directory.
        for(MediaElement child : getChildrenOf(directory.getPath(), true, true, false, false))
        { 
            // Check that a year value exists for the current media element.
            if(child.getMetaData().getYear() != null)
            {
                // Update the current year variable.
                currentYear = child.getMetaData().getYear();
                
                // Check that the year value is real.
                if(currentYear > 0)
                {
                    // Set an initial year to compare.
                    if(year <= 0)
                    {
                        year = currentYear;
                    }

                    // Compare the current year with the initial year.
                    // If year values don't match, exit the loop.
                    if(currentYear.intValue() != year.intValue())
                    {
                        commonYear = false;
                        break;
                    }
                }
            }
        }
        
        // If the year of all child media elements match, return that year.
        if(commonYear && year > 0)
        {
            return year;
        }
        
        // Return null as a common year was not found for the given directory.
        return null;
    }
    
    private MediaElementType getMediaElementType(File file)
    {
        if(file.isFile())
        {
            if(isAudioFile(file.getPath()))
                return MediaElementType.AUDIO;
            else if (isVideoFile(file.getPath()))
                return MediaElementType.VIDEO;
            else
                return null;
        }
        
        if(file.isDirectory())
        {
            if(getDirectoryMediaType(file) == DirectoryMediaType.NONE)
            {
                return MediaElementType.DIRECTORY;
            }
            
            return MediaElementType.MEDIA_DIRECTORY;
        }
        
        return null;
    }
    
    private boolean isAudioFile(String path) 
    {
        for (String s : settingsService.getMusicFileTypesAsArray()) {
            if (path.endsWith(s.toLowerCase())) {
                return true;
            }
        }
        return false;
    }

    private boolean isVideoFile(String path) 
    {
        for (String s : settingsService.getVideoFileTypesAsArray()) {
            if (path.endsWith(s.toLowerCase())) {
                return true;
            }
        }
        return false;
    }

    /**
     * Returns a cover art image for the given media element.
     */
    public File getCoverArt(MediaElement mediaElement) 
    {
        // Retrieve Meta-data
        mediaElement.setMetaData(getMetaData(mediaElement));
        
        if (mediaElement.getMetaData().getCoverArtFile() != null)
        {
            return mediaElement.getMetaData().getCoverArtFile();
        }
        
        MediaElement parent = getParentOf(mediaElement);
        return parent == null ? null : parent.getMetaData().getCoverArtFile();
    }
    
    /**
     * Finds a cover art image in the given path.
     */
    private String findCoverArt(String path) 
    {        
        // Recursively check for cover art in given directory path
        for (String name : settingsService.getCoverArtFileTypesAsArray()) 
        {
            for (File candidate : new File(path).listFiles()) 
            {
                if (candidate.isFile() && !FileUtil.isHidden(candidate) && candidate.getName().toUpperCase().endsWith(name.toUpperCase())) 
                {
                    return candidate.getPath();
                }
            }
        }
        
        // Failed to find cover art in given path
        return null;
    }
    
     /**
     * Returns a cover art image for the given media element.
     */
    public File getPoster(MediaElement mediaElement)
    {
        mediaElement.setMetaData(getMetaData(mediaElement));
        
        if (mediaElement.getMetaData().getPosterFile() != null)
        {
            return mediaElement.getMetaData().getPosterFile();
        }
        
        MediaElement parent = getParentOf(mediaElement);
        return parent == null ? null : parent.getMetaData().getPosterFile();
    }
    
     /**
     * Finds a poster image for the given directory, by looking for it on the
     * disk.
     */
    private String findPoster(String path)
    {
        // Recursively check for poster art in given directory path
        for (String name : settingsService.getPosterFileTypesAsArray()) 
        {
            for (File candidate : new File(path).listFiles()) 
            {
                if (candidate.isFile() && !FileUtil.isHidden(candidate) && candidate.getName().toUpperCase().endsWith(name.toUpperCase())) 
                {
                    return candidate.getPath();
                }
            }
        }
        
        // Failed to find poster art in given path
        return null;
    }
    
     /**
     * Returns a subtitles for the given media file.
     */
    public File getSubtitles(MediaElement mediaElement)
    {
        mediaElement.setMetaData(getMetaData(mediaElement));
        
        if (mediaElement.getMetaData().getSubtitlesFile() != null)
        {
            return mediaElement.getMetaData().getSubtitlesFile();
        }
        
        MediaElement parent = getParentOf(mediaElement);
        return parent == null ? null : parent.getMetaData().getSubtitlesFile();
    }
    
     /**
     * Finds subtitles for the given directory, by looking for it on the
     * disk.
     */
    private String findSubtitles(String path)
    {
        // Recursively check for cover art in given directory path
        for (String name : settingsService.getSubtitlesFileTypesAsArray()) 
        {
            for (File candidate : new File(path).listFiles()) 
            {
                if (candidate.isFile() && !FileUtil.isHidden(candidate) && candidate.getName().toUpperCase().endsWith(name.toUpperCase())) 
                {
                    return candidate.getPath();
                }
            }
        }
        
        // Failed to find subtitles in given path
        return null;
    }

    /**
     * Returns all media elements that are children, grand-children etc of a given
     * media element. Directories are not included in the result.
     *
     * @param sort Whether to sort files in the same directory.
     * @return All descendant Media Elements.
     */
    public List<MediaElement> getDescendantsOf(MediaElement ancestor, boolean sort) {

        if (!ancestor.isDirectory())
        {
            return Arrays.asList(ancestor);
        }

        List<MediaElement> result = new ArrayList<>();

        for (MediaElement child : getChildrenOf(ancestor.getPath(), true, true, true, sort))
        {
            // Retrieve Meta-data
            child.setMetaData(getMetaData(child));
            
            if(child.isDirectory())
            {
                result.addAll(getDescendantsOf(child, sort));
            } 
            else 
            {
                result.add(child);
            }
        }
        
        return result;
    }
    
    /**
     * Returns the name of the highest level parent directory for a given file.
     * 
     * @param file The file in question.
     * @return The name of the highest level parent directory.
     */
    public String getHighLevelDirectoryName(File file) 
    {        
        while(file.getParentFile() != null)
        {                      
            if(settingsService.isMediaFolder(file.getParent()))
            {
                if(file.isDirectory())
                {
                    return file.getName();
                }
                else
                {
                    return "";
                }
            }
            
            file = file.getParentFile();
        }
        
        return "";
    }

    public void updateMediaElement(MediaElement mediaElement) 
    {
        mediaElementDao.createOrUpdate(mediaElement);
    }

    /**
     * Increments the play count and last played date for the given media element
     * and its parent media element.
     */
    public void incrementPlayCount(MediaElement element) {
        Date now = new Date();
        element.setLastPlayed(now);
        element.setPlayCount(element.getPlayCount() + 1);
        updateMediaElement(element);

        MediaElement parent = getParentOf(element);
        if (!settingsService.isMediaFolder(parent.getPath())) {
            parent.setLastPlayed(now);
            parent.setPlayCount(parent.getPlayCount() + 1);
            updateMediaElement(parent);
        }
    }
    
    //
    // Meta-Data Utilities
    //
    
    public MetaData getMetaData(MediaElement mediaElement)
    {
        if(mediaElement ==  null)
        {
            return null;
        }
        
        switch(mediaElement.getMediaElementType())
        {
            case AUDIO:
                return audioMetaDataDao.getMetaData(mediaElement.getId());
                
            case VIDEO:
                return videoMetaDataDao.getMetaData(mediaElement.getId());
                
            case MEDIA_DIRECTORY:
                MetaData metaData = directoryMetaDataDao.getMetaData(mediaElement.getId());
                Integer year = getDirectoryYear(mediaElement);
                
                if(year != null)
                {
                    metaData.setYear(year);
                }
                
                return metaData;
                
            case DIRECTORY:
                return directoryMetaDataDao.getMetaData(mediaElement.getId());
                
            default:
                return null;
        }
    }
    
    //
    // Services & Database Managers
    //
    
    public void setSecurityService(SecurityService securityService) {
        this.securityService = securityService;
    }

    public void setSettingsService(SettingsService settingsService) {
        this.settingsService = settingsService;
    }

    public void setMediaElementDao(MediaElementDao mediaElementDao) {
        this.mediaElementDao = mediaElementDao;
    }
    
    public void setAudioMetaDataDao(AudioMetaDataDao audioMetaDataDao) {
        this.audioMetaDataDao = audioMetaDataDao;
    }
    
    public void setVideoMetaDataDao(VideoMetaDataDao videoMetaDataDao) {
        this.videoMetaDataDao = videoMetaDataDao;
    }
    
    public void setDirectoryMetaDataDao(DirectoryMetaDataDao directoryMetaDataDao) {
        this.directoryMetaDataDao = directoryMetaDataDao;
    }
    
    public void setAudioParser (AudioParser audioParser) {
        this.audioParser = audioParser;
    }
    
    public void setVideoParser(VideoParser videoParser) {
        this.videoParser = videoParser;
    }
}
